﻿using GitHubCopilotProxies.Handlers;

using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Options;

using Serilog;
using Serilog.Events;

using System.Collections.Frozen;
using System.Net;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using System.Text;

namespace GitHubCopilotProxies;

public class Program
{
    public static void Main(string[] args)
    {
        if (File.Exists(".env"))
        {
            var lines = File.ReadAllLines(".env")
               .Where(line => !string.IsNullOrWhiteSpace(line) && !line.StartsWith("#"));
            foreach (var line in lines)
            {
                var parts = line.Split('=', 2);
                if (parts.Length == 2)
                    Environment.SetEnvironmentVariable(parts[0].Trim(), parts[1].Split('#').First().Trim());
            }
        }

        var builder = WebApplication.CreateSlimBuilder(args);
        #region 监听地址
        var urls = Environment.GetEnvironmentVariable("ASPNETCORE_URLS")?.Split(';').ToHashSet() ?? new HashSet<string>();
        var httpPort = Environment.GetEnvironmentVariable("HTTP_PORT");
        if (string.IsNullOrWhiteSpace(httpPort) == false)
            urls.Add($"http://[::]:{httpPort}");
        var httpsPort = Environment.GetEnvironmentVariable("HTTPS_PORT");
        if (string.IsNullOrWhiteSpace(httpsPort) == false)
            urls.Add($"https://[::]:{httpsPort}");
        if (urls is { Count: 0 }) urls.Add("https://[::]:443");
        builder.WebHost.UseUrls(urls.ToArray());
        #endregion
        builder.WebHost.UseKestrelHttpsConfiguration();
        builder.WebHost.ConfigureKestrel(serverOptions =>
        {
            X509Certificate2? cert = null;
            async Task DownloadCertFilesAsync()
            {
                using var httpClient = new HttpClient() { BaseAddress = new Uri("https://data-1251486259.cos.ap-beijing.myqcloud.com/copilot-ssl/") };
                async Task DownloadCertFileAsync(string filename)
                {
                    var fileBytes = await httpClient.GetByteArrayAsync(filename);
                    await File.WriteAllBytesAsync($"cert/{filename}", fileBytes);
                }
                Directory.CreateDirectory("cert");
                await DownloadCertFileAsync("ssl.pem");
                await DownloadCertFileAsync("ssl.key");
            }
            const string pemPath = "cert/ssl.pem";
            const string keyPath = "cert/ssl.key";
            try
            {
                void DownloadCert()
                {
                    DownloadCertFilesAsync().GetAwaiter().GetResult();
                    cert = X509Certificate2.CreateFromPemFile(pemPath, keyPath);
                }
                if (File.Exists(pemPath) && File.Exists(keyPath))
                {
                    cert = X509Certificate2.CreateFromPemFile(pemPath, keyPath);
                    if ((cert.NotAfter - DateTime.Today).TotalDays < 30)
                        DownloadCert();
                }
                else DownloadCert();
            }
            catch { }
            if (cert is not null)
            {
                cert = X509CertificateLoader.LoadPkcs12(cert.Export(X509ContentType.Pkcs12), null);
                serverOptions.ConfigureHttpsDefaults(httpsOptions => httpsOptions.ServerCertificate = cert);
            }
        });

        #region 配置注入
        builder.Services.Configure<SystemConfig>(builder.Configuration.GetSection("SystemConfig"));
        var codeCompletionsCache = new ConfigCache<SystemConfig.CodeCompletionsConfig>();
        var editCompletionsCache = new ConfigCache<SystemConfig.CodeCompletionsConfig>();
        var embeddingsCache = new ConfigCache<SystemConfig.BaseModelConfig>();
        var chunksCache = new ConfigCache<SystemConfig.BaseModelConfig>();
        var modelsCache = new ConfigCache<string>();
        var chatCompletionsCache = new ConfigCache<IReadOnlyDictionary<string, SystemConfig.ChatCompletionsConfig>>();
        builder.Services.AddKeyedScoped<ConfigCache<SystemConfig.CodeCompletionsConfig>>("CodeCompletions", (_, _) => codeCompletionsCache);
        builder.Services.AddKeyedScoped<ConfigCache<SystemConfig.CodeCompletionsConfig>>("EditCompletions", (_, _) => editCompletionsCache);
        builder.Services.AddKeyedScoped<SystemConfig.BaseModelConfig>("Embeddings", (_, _) => embeddingsCache.Get());
        builder.Services.AddKeyedScoped<SystemConfig.BaseModelConfig>("Chunks", (_, _) => chunksCache.Get());
        builder.Services.AddKeyedScoped<string>("Models", (_, _) => modelsCache.Get());
        builder.Services.AddScoped<IReadOnlyDictionary<string, SystemConfig.ChatCompletionsConfig>>(_ => chatCompletionsCache.Get());

        builder.Services.AddHttpClient(string.Empty)
            .ConfigurePrimaryHttpMessageHandler(_ => new HttpClientHandler() { Proxy = null, UseProxy = false });
        builder.Services.AddHttpClient("Proxy")
            .ConfigurePrimaryHttpMessageHandler((service) =>
            {
                var proxyAddress = service.GetRequiredService<IConfiguration>().GetValue<string>("ProxyAddress");
                if (string.IsNullOrWhiteSpace(proxyAddress)) return new HttpClientHandler();
                return new HttpClientHandler { Proxy = new WebProxy(proxyAddress), UseProxy = true };
            });
        #endregion

        builder.Host.UseSerilog((context, configuration) =>
        {
            var isDev = context.HostingEnvironment.IsDevelopment();
            //configuration.MinimumLevel.Verbose();
            //configuration.Enrich.FromLogContext();
            configuration.MinimumLevel.Override("Microsoft", LogEventLevel.Information);
            configuration.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning);
            configuration.MinimumLevel.Override("Microsoft.AspNetCore.Hosting.Diagnostics", isDev ? LogEventLevel.Information : LogEventLevel.Warning);
            configuration.MinimumLevel.Override("System.Net.Http.HttpClient", isDev ? LogEventLevel.Debug : LogEventLevel.Error);
#if !DEBUG
            configuration.Filter.ByExcluding(Serilog.Filters.Matching.WithProperty<string>("RequestPath", path =>
            {
                var currPath = PathString.FromUriComponent(path);
                return currPath.StartsWithSegments("/help") || currPath.StartsWithSegments("/api") || currPath.StartsWithSegments("/site") ||
                    currPath.StartsWithSegments("/agents") || currPath.StartsWithSegments("/teams") || currPath.StartsWithSegments("/user") ||
                    currPath.StartsWithSegments("/_ping") || currPath.StartsWithSegments("/telemetry");
            }));
#endif
            configuration.WriteTo.Console(restrictedToMinimumLevel: isDev ? LogEventLevel.Verbose : LogEventLevel.Information);
            configuration.WriteTo.File(
                Path.Combine(context.HostingEnvironment.ContentRootPath, "logs", @".log"),
                restrictedToMinimumLevel: isDev ? LogEventLevel.Information : LogEventLevel.Warning,
                rollingInterval: RollingInterval.Day,
                retainedFileCountLimit: 7,
                shared: true
            );
        });

        builder.Services.AddMemoryCache();
        builder.Services.AddHttpContextAccessor();
        builder.Services.ConfigureHttpJsonOptions(options =>
        {
            options.SerializerOptions.Encoder = AppJsonSerializerContext.Web.Options.Encoder;
            options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Web);
        });
        builder.Services.AddSingleton(new EmbeddedFileProvider(Assembly.GetEntryAssembly()!));

        var app = builder.Build();
        app.UseSerilogRequestLogging();

        var sysCfg = app.Services.GetRequiredService<IOptionsMonitor<SystemConfig>>();
        ReloadConfig(sysCfg.CurrentValue);
        sysCfg.OnChange((config, name) => ReloadConfig(config));
        void ReloadConfig(SystemConfig config)
        {
            codeCompletionsCache.Update(config.CodeCompletions!.InitRoundRobin());
            editCompletionsCache.Update(SystemConfig.WithDefault(config.EditCompletions, config.CodeCompletions!).InitRoundRobin());
            embeddingsCache.Update(config.Embeddings!.InitRoundRobin());
            chunksCache.Update(SystemConfig.WithDefault(config.Chunks, config.Embeddings!).InitRoundRobin());
            var (modelIds, modelJson) = Utils.ReloadModels(config, app.Services.GetRequiredService<EmbeddedFileProvider>());
            modelsCache.Update(modelJson);
            chatCompletionsCache.Update(modelIds.ToFrozenDictionary(id => id, id => config.GetChatCompletionsConfig(id)!.InitRoundRobin()));
        }

        app.MapPost("/login/device/code", AuthGitHub.PostLoginDeviceCodeAsync);
        app.MapGet("/login/device", AuthGitHub.UserLoginDeviceAsync);
        app.MapGet("/login/oauth/authorize", AuthGitHub.GetLoginOauthAuthorizeAsync);
        app.MapPost("/login/oauth/access_token", AuthGitHub.GetLoginOauthAccessTokenAsync);
        app.MapGet("/site/sha", AuthGitHub.GetSiteShaAsync);

        app.MapGet("/user", User.GetLoginUser);
        app.MapGet("/user/orgs", User.GetUserOrgs);
        app.MapGet("/api/v3/user", User.GetLoginUser);
        app.MapGet("/api/v3/user/orgs", User.GetUserOrgs);
        app.MapGet("/teams/{teamID}/memberships/{username}", User.GetMembership);
        app.MapGet("/copilot_internal/user", User.GetCopilotInternalUser);
        app.MapGet("/copilot_internal/v2/token", User.GetDisguiseCopilotInternalV2Token);

        app.MapPost("/v1/engines/{model}/speculation", CopilotCompletions.CodeCompletions);
        app.MapPost("/v1/engines/{model}/completions", CopilotCompletions.CodeCompletions);
        app.MapPost("/v1/engines/copilot-codex", CopilotCompletions.CodeCompletions);
        app.MapPost("/v1/chat/completions", CopilotCompletions.ChatCompletions);
        app.MapPost("/chat/completions", CopilotCompletions.ChatCompletions);
        app.MapPost("/agents/chat", CopilotCompletions.ChatCompletions);
        app.MapPost("/chunks", CopilotCompletions.HandleChunks);
        app.MapPost("/embeddings", CopilotCompletions.HandleEmbeddings);

        app.MapGet("/telemetry", Telemetry.GetTelemetryAsync);
        app.MapPost("/telemetry", Telemetry.PostTelemetryAsync);

        app.MapGet("/help", Utils.Help);
        app.MapGet("/models", Utils.GetModels);
        app.MapGet("/_ping", Utils.GetPing);
        app.MapGet("/agents", Utils.GetAgents);
        app.MapGet("/api/v3/meta", Utils.GetV3meta);
        app.MapGet("/api/v3/", Utils.GetCliv3);
        app.MapGet("/", Utils.GetCliv3);

        app.Use(async (context, next) =>
        {
            context.Request.EnableBuffering();
            await next();
            if (context.Response.StatusCode == 404)
                if (!Path.HasExtension(context.Request.Path.Value))
                {
                    var stringBuilder = new StringBuilder();
                    foreach (var header in context.Request.Headers)
                        stringBuilder.AppendLine($"{header.Key}: {header.Value}");
                    var requestHeader = stringBuilder.ToString();
                    var requestParams = context.Request.QueryString.ToString();
                    string requestBody = "";
                    try
                    {
                        context.Request.Body.Position = 0;
                        using var reader = new StreamReader(context.Request.Body);
                        requestBody = await reader.ReadToEndAsync();
                        context.Request.Body.Position = 0;
                    }
                    catch { }
                    Log.Warning("捕获未被正确处理的 404 请求: {Method} {Path} \r\n请求头：\r\n{Header}\r\n参数: \r\n{Params} \r\n请求体: \r\n{RequestBody}\r\n",
                                context.Request.Method, context.Request.Path, requestHeader, requestParams, requestBody);
                }
        });

        app.Run();
    }
}

